# -*- coding: utf-8 -*-
'''
Created on 21 mars 2011

@author: bressu
'''

import os
import os.path
import time
import random

N900_PUSH_DIR = "/home/thierry/workspace/web900/server/apache/www/push"
N900_COMMAND_DIR = "/home/thierry/workspace/web900/server/apache/www/poll"
N900_UPLOAD_DIR = "/home/thierry/workspace/web900/server/apache/www/upload"

import model


class AuthorizationException(Exception):
    pass

class ObjectNotFound(Exception):
    pass

class TooManyObjectFound(Exception):
    pass


def _getDevice(user):
    """
    return the device for an user
    """
    found = [d for d in  model.Device.read(user.deviceId, cnx=None)]
    if len(found) == 0:
        raise ObjectNotFound()
    if len(found) > 1:
        raise TooManyObjectFound()        
    return found[0]

class Component():
    """
    Base classe for any feature of web900. Make authorization checks.
    """
    
    def __init__(self, user):
        # check if the user can use this feature
        if not user:
            raise AuthorizationException()
        
        self.user = user
        
    def _getOwnedDevice(self, strict=False):
        """
        Return the device owned or used by the current user. The device can either be owned by the user
        either only shared with this user by the device owber.
        if strict is True return the device only if the current user is the real owner of the device. 
        """         
        
        
        device = _getDevice(self.user)
        if strict and ( device.owner != self.user.login):
            raise AuthorizationException()
        return device       
    
    def _isRealOwner(self, user):
        """
        Test if the is a real owner of a device and not a guess
        """
        try:
            self._getOwnedDevice(strict=True)
        except AuthorizationException:
            return False
        else:
            return True
            
class Filemanager(Component):
    """
    Upload and download file to/from the device    
    """

    def __init__(self, user):
        Component.__init__(self, user)
    
    def extract(self, filename):
        letters = ['a','b','c','d','e,','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']         
        numbers = ['0','1','2','3','4','5','6','7','8','9']
        elements = letters +  numbers
        SIZE = 16
        scrumbled = ""
        for i in range(SIZE):
            scrumbled += random.choice(elements)         
        f = open(os.path.join(N900_UPLOAD_DIR, scrumbled),'w')
        for byte in  self.get(filename):
            f.write(byte)
        f.close()
        return scrumbled   
    
    def get(self, filename):
        device = self._getOwnedDevice()
        for d in model.UploadedFile.read(device.id, filename, cnx=None):
            for byte in d.content:
                yield byte

    def getPendingDownload(self):
        device = self._getOwnedDevice()
        for f in model.UploadedFile.read(device.id, cnx=None):
            yield f
        
    def delete_uploaded_file(self, filename, filekey):
        """
        The device use this action to delete an uploaded file
        after it have successfully download it 
        """
        device = self._getOwnedDevice()
        os.remove(os.path.join(N900_UPLOAD_DIR, filekey))
        uf = model.UploadedFile()
        uf.deviceId = device.id
        uf.name = filename
        uf.delete(cnx=None)
         
     
        
    
    def upload_file(self, file, filename):
        """
        This action is used to push a file on the device.
        """
        device = self._getOwnedDevice()
        # copy the file in the upload dir        
        uf = model.UploadedFile()
        uf.deviceId = device.id
        uf.name = filename
        uf.content = buffer(file.read()) # TODO use a buffered, file.read() return the whole data...
        uf.create(cnx=None)
        # post a command to tell the device to upload these file
        cq = CommandQueue(self.user)
        cq.post_upload_cmd(filename)

    
class AddressBook(Component):
    """
    Manage contact of the device
    """



    def __init__(self, user):
        Component.__init__(self, user)            



    def getContact(self,uid):
        """
        Return a contact specified by its uid for the device of the current user
        """
        device = self._getOwnedDevice()
        found = [ c for c in model.Contact.read(device.id,uid, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return found[0]
        
    def getContactField(self,contactId, name):
        """
        Return the contact field identified by the contactId and the name of the field.
        The contact is search in the current user address book
        """        
        found = [ c for c in model.ContactField.read(contactId, name=name, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return found[0]
        
    def push_address_book(self, **kwargs):
        device = self._getOwnedDevice()
        uid = kwargs['uid']
        try:
            c = self.getContact(uid)
        except ObjectNotFound:
            # creation
            c = model.Contact()
            c.deviceId = device.id
            c.uid = uid
            c.create(cnx=None)                    
            
            keys = kwargs.keys()
            for kw in keys:
                cf = model.ContactField()
                cf.contactId = c.id
                cf.name  = kw
                cf.value = kwargs[kw]
                cf.create(cnx=None)
        else:
            # update
            keys = kwargs.keys()
            for kw in keys:
                try:
                    cf = self.getContactField(c.id, kw)
                except ObjectNotFound:
                    # create
                    cf = model.ContactField()
                    cf.contactId = c.id
                    cf.name  = kw
                    cf.value = kwargs[kw]
                    cf.create(cnx=None)
                else:
                    #update
                    cf.value = kwargs[kw]
                    cf.update(cnx=None)
        
    def generate_address_book(self):
        device = self._getOwnedDevice()
        for c in model.Contact.read(device.id, cnx=None):         
            contact = {}
            for field in model.ContactField.read(c.id, name=None, cnx=None):                
                contact[field.name] = field.value               
            yield contact
    
    
class Application(Component):
    """
    Manager interaction with device application such as webbrower, multimedia player
    """
    def __init__(self, user):
        Component.__init__(self, user)            
                                        
    def open_url(self, url):
        CommandQueue(self.user).post_open_url_cmd(url)

class Phone(Component):
    """
    Manage phone function : call and sms
    """
    
    def __init__(self, user):
        Component.__init__(self, user)    
    
    def send_sms(self, recipient, content):
        CommandQueue(self.user).post_send_sms_cmd(recipient, content)

    def call(self, number):
        CommandQueue(self.user).post_call_cmd(number)
        
    def ring_my_bell(self):
        CommandQueue(self.user).post_ring_my_bell()

class Device(Component):
    """
    Manage device status and command
    """
    def __init__(self, user):
        Component.__init__(self, user)
            
        
    def lock_device(self):
        CommandQueue(self.user).post_lock_device_cmd()


    def unlock_device(self):
        CommandQueue(self.user).post_unlock_device_cmd()        
        
    def shutdown(self):
        CommandQueue(self.user).post_shutdown_cmd()
        
    def getOwnedDevice(self):
        """
        Return the device owned by the current user
        """
        return self._getOwnedDevice(strict=True)

    def getStatus(self):
        """
        Return a dictionnary of statuses variables
        """
        resu = {}
        device = self._getOwnedDevice()
        for sf in model.Status.read(device.id, cnx=None):                                            
            resu[sf.name] = sf.value                           
        return resu            


    def getStatusField(self, name):
        device = self._getOwnedDevice()
        found =  [ f for f in model.Status.read(device.id, name=name, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return found[0]
    
    def push_status(self, data):
        device = self._getOwnedDevice()
        for name in data.keys():
            try:
                sf = self.getStatusField(name)
            except ObjectNotFound:
                # create
                sf = model.Status()
                sf.deviceId  = device.id
                sf.name = name
                sf.value = data[name]
                sf.create(cnx=None)
            else:
                # update
                sf.value = data[name]
                sf.update(cnx=None)
            
                       

           
    def push_location(self, lat ,long, fix):       
        device = self._getOwnedDevice()
        try:
            loc = self.get_location()
        except ObjectNotFound:
            # create
            loc = model.Location()
            loc.deviceId = device.id
            loc.lat = lat
            loc.long = long
            loc.fix = fix
            loc.create(cnx=None)
        else:
            #update
            loc.lat = lat
            loc.long = long
            loc.fix = fix
            loc.update(cnx=None)
        
    def get_location(self):
        device = self._getOwnedDevice()
        found = [f for f in model.Location.read(deviceId=device.id, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return found[0]
    
    def display_im(self, content):
        """
        Send a messaeg notification e.g. instant messaging to the device
        """
        CommandQueue(self.user).post_instant_message(content)
        
        
    
class CommandQueue(Component):
    """
    Command posted to the device
    """
    
    UPLOAD_CMD = "upload"
    LOCK_CMD = "lock"
    UNLOCK_CMD = "unlock"
    SMS_CMD = "sms"
    CALL_CMD = "call"
    OPEN_URL_CMD = "url"
    IM_CMD = "im"
    RING_CMD = "ring"
    
    def __init__(self, user):
        Component.__init__(self, user)    
        
    
    def post_ring_my_bell(self):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.RING_CMD
        c.timestamp = time.time()        
        c.create(cnx=None)
    
    def post_lock_device_cmd(self):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.LOCK_CMD
        c.timestamp = time.time()        
        c.create(cnx=None)
        
    def post_unlock_device_cmd(self):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.UNLOCK_CMD
        c.timestamp = time.time()        
        c.create(cnx=None)

    def post_shutdown_cmd(self):
        pass # TODO

    
    def post_upload_cmd(self, filename):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.UPLOAD_CMD
        c.timestamp = time.time()
        c.arguments = filename
        c.create(cnx=None)
        
    def post_instant_message(self, content):
        device = self._getOwnedDevice(strict=False)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.IM_CMD
        c.timestamp = time.time()
        # warning content may be a unicode string
        try:
            content = content.encode('utf-8')
        except:
            raise
        c.arguments = unicode(self.user.login) + u":" + unicode(content)
        c.create(cnx=None)
        
        
    def post_send_sms_cmd(self, recipient, content):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.SMS_CMD
        c.timestamp = time.time()
        # warning content may be a unicode string
        try:
            content = content.encode('utf-8')
        except:
            pass
        c.arguments = recipient + u":" + content
        c.create(cnx=None)
        
    def post_call_cmd(self, number):
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.CALL_CMD
        c.timestamp = time.time()
        c.arguments = number
        c.create(cnx=None) 
        
    
    def post_open_url_cmd(self, url):    
        device = self._getOwnedDevice(strict=True)
        c = model.Command()
        c.deviceId = device.id
        c.name = self.OPEN_URL_CMD
        c.timestamp = time.time()
        c.arguments = url
        c.create(cnx=None) 
        
        
    def poll_command(self):
        """
        Retrieve all command and return an xml. Retrieved command are removed from the Queue
        """
        device = self._getOwnedDevice(strict=True)
        bulk = "<?xml version=\"1.0\" ?>"
        bulk = bulk + "<commands>"        
        for cmd in model.Command.read(device.id, cnx=None) :
        
                if cmd.name == self.IM_CMD:
                    sender, content = cmd.arguments.split(":")                                                            
                    guess = None                    
                    if sender == self.user.login:
                        # the owner can snt itself a IM ???
                        guess = self.user
                    else:
                        # if the sender is not the device owner
                        # sender MUST be a still valid guess
                        for g in AccountManager(self.user).getGuess():
                            if g.login == sender :
                                guess = g
                        if not guess:
                            # this se,der is not allowed any more to im the current user
                            break                     
                    bulk = bulk + "<command id=\'" + cmd.name + "\'>" 
                    bulk = bulk + "<sender>"
                    bulk = bulk + "<login>" + guess.login + "</login>"
                    bulk = bulk + "<name>" + guess.name + "</name>"                    
                    bulk = bulk + "</sender>"
                    bulk = bulk + "<content>" + content + "</content>"
                    bulk = bulk + "</command>"
                else:                            
                    bulk = bulk + "<command id=\'" + cmd.name + "\'>"            
                    if cmd.name == self.SMS_CMD:
                        recipient, content = cmd.arguments.split(":")                                        
                        bulk = bulk + "<recipient>" + recipient + "</recipient>"
                        bulk = bulk + "<content>" + content + "</content>"
                    elif cmd.name == self.CALL_CMD:
                        number = cmd.arguments 
                        bulk = bulk + "<number>" + number + "</number>"
                    elif  cmd.name == self.OPEN_URL_CMD:
                        url = cmd.arguments 
                        bulk = bulk + "<url>" + url + "</url>"
                    elif cmd.name == self.UPLOAD_CMD:
                        filename = cmd.arguments
                        # extract the file from the storage and push it a webserver path
                        fm = Filemanager(self.user)   
                        scrumbled = fm.extract(filename)
                        bulk = bulk + "<filename>" + filename + "</filename>"            
                        bulk = bulk + "<filekey>" + scrumbled + "</filekey>"                                 
                    bulk = bulk + "</command>"        
                cmd.delete(cnx=None)
        bulk = bulk + "</commands>"
        return bulk

    
class AuthorizationManager(Component):
    """
    Manager of authorization. Use this component when  you want to give permission on some component to "guest" user.
    Guest user are user who are not owner of a device.
    """
    
    def __init__(self, user):
        Component.__init__(self, user)


    def check_perm(self, feature):
        if feature == 'api_key':
            try:
                self._getOwnedDevice(strict=True)
            except:
                return False
            
        
        return True
            
        


def generate_device_key():
    """
    Generate a pseudo random key that can be associated to a device
    """
    letters = ['a','b','c','d','e,','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
    upper_letters = [ c.upper() for c in letters]
    numbers = ['0','1','2','3','4','5','6','7','8','9']
    elements = letters + upper_letters + numbers
    SIZE = 16
    resu = ""
    for i in range(SIZE):
        resu += random.choice(elements) 
    return resu
        
class AccountManager(Component):
    """
    This object is used to create, retrieve, modify or delete account.
    """
    
    def __init__(self, user):
        
        # this component need to be instanciated when there is no loggued user to allow account creation !
        class DummyUser(model.User):
            pass                 
        if not user:
            user = DummyUser()
            
        Component.__init__(self, user)
    
    
    
    def delete_guess(self, guess):
        """
        Delete a guess user
        """
        if self._isRealOwner(self.user):
            for user_guess in self.getGuess():
                if user_guess.login == guess:
                    user_guess.delete(cnx=None)
        else:
            raise AuthorizationException()
        
        
        
    def create_guess(self,login, password):
        """
        Create a new guess user for the current user account.
        """
        u = model.User()
        u.login = login
        u.password = password
        u.deviceId = self._getOwnedDevice(strict=True).id        
        u.name = ""
        u.create(cnx=None)  # should be called with an external trx context
      
        
        # should commit the external trx context
        return u
    
    
    def getGuess(self):
        """
        Retrieve all user who are guess on the device of owned by current user.
        This is a generator
        """
        for u in model.User.read_guess(self.user.login, cnx=None):
            yield u
    
    def create(self, login, password):
        """
        Create a new account on the system
        """           
      
            
        d = model.Device()
        d.owner = login
        d.key = generate_device_key()
        d.create(cnx=None) # should be called with an external trx context
        
        u = model.User()
        u.login = login
        u.password = password
        u.deviceId = d.id # the create call on the Device has filled this attribute        
        u.name = ""
        u.create(cnx=None)  # should be called with an external trx context
      
        
        # should commit the external trx context
        return u
    
    
    
    def update_user(self, name, password):
        """
        Upadte current user.
        """
        self.user.name = name
        self.user.password = password
        self.user.update(cnx=None)
        return self.user
    
    def change_api_key(self):
        """
        Change the api key by a new generated one
        """
        device = self._getOwnedDevice(strict=True)
        device.key= generate_device_key()
        device.update(cnx=None)
        
    
    def delete(self, login):
        """
        Delete an account. 
        """
    
    def getUserFromApiKey(self, key):        
        found = [u for u in model.Device.read(key=key, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return self._get(found[0].owner)
    
    def validate(self, login, password):
        """
        Check if the login and password are valid e.g. there is an existing account 
        """
        resu = None
        try:
            u = self._get(login)
        except:
            pass
        else:
            if u.password == password:
                resu = u
        return resu
        
    def _get(self, login):
        """
        Retrieve an existing account.
        If there is no user or more than one for the given login then an exception is thrown
        """
        found = [u for u in model.User.read(login, cnx=None)]
        if len(found) == 0:
            raise ObjectNotFound()
        if len(found) > 1:
            raise TooManyObjectFound()
        return found[0]
        
    
    def getAllAccount(self):
        """
        Return all account on the system.
        This is a generator       
        """
        if not self.user.admin  :
            raise AuthorizationException()   
        for u in model.User.read(login=None, cnx=None):
            yield u
            
    def getDevice(self, user):
        """
        Return the device for an user.    
        Only admin can request this method  
        """ 
        
        # if not admin only own used device can be requested       
        if  not self.user.admin :
            raise AuthorizationException()        
        return _getDevice(user)
 
        
        